Repository navigation
fix: resolve CSS-wide keywords and the inherited colour as CSS defines them - #391
Draft
YevheniiKotyrlo wants to merge 28 commits into
Draft
YevheniiKotyrlo wants to merge 28 commits into
YevheniiKotyrlo wants to merge 28 commits into
Conversation
YevheniiKotyrlo
marked this pull request as ready for review
July 26, 2026 14:53
4 tasks done
mubeess
added a commit
to mubeess/react-native-css
that referenced
this pull request
Jul 31, 2026
- Hoist allowed Set to module scope (no per-call realloc) - Comment lightningcss case normalization and nativewind#391 asymmetry - Add negative test: match-parent drops with warning (no over-match) - Document native View->Text inheritance caveat in README
This was referenced Aug 15, 2026
Contributor
Author
`color: inherit`, `unset` and `currentcolor` compile to a read of the colour every `color` rule publishes, and a colour that reads that channel is not published back into it. While an element computes its own `color`, the read resolves in the scope it inherited, because css-color-4 makes `currentcolor` the inherited colour there; on every other property it stays the element's own colour. nativewind#451 ordered the element's own variables before inherited ones, so the inherited read is now explicit rather than a consequence of lookup order.
The compiler and the resolver each asked whether a var() reads the inherited colour, under one name with two meanings. The shallow question is now `isInheritedColorLookup` beside the variable it names, and the compiler's walk calls it for each var() it meets. Each constraint is stated in one line, citing the sections that state it.
No caller passes parseUnparsed a string or a number: with either removed from the parameter type, tsc reports no call site, and the test suite never takes either branch. The string branch's `currentcolor` arm was a third spelling of the inherited-colour lookup that nothing reached.
Each expected value is what Chromium, Firefox and WebKit compute for the same CSS. The cases that pinned a divergence are gone: a light-dark() descendant in dark mode, a custom property holding `inherit` that is not inlined, and the string relative colour syntax resolves to. `::selection` is left to the pseudo-element mapping, and `::placeholder` also reads the element's own colour.
A CSS-wide keyword is a value only on its own (css-values-4 §2.1), so the compiler resolves one where it is a declaration's whole value or a var() fallback's, and rejects one inside a longer value. `revert` and `revert-layer` roll back to `unset`, as React Native has no user-agent or user origin. `unset` is `inherit` on `color` and on a custom property and `initial` elsewhere, and `initial` clears the property to React Native's default. A custom property left to inherit publishes nothing, so a read finds its ancestor's value, and its warnings now name it. A shorthand cleared by a keyword resets every longhand it sets, and a cleared filter is none. An element's inherited colour comes from its last rule that publishes one, so a later `color` that inherits no longer leaves an earlier rule's colour for its subtree, and a pseudo-element's colour stays its own.
The first pass prints the stylesheet before the second parses it, so no white space from a comment reaches the keyword, and the filter for it was unreachable.
A shorthand parsed at runtime was registered with an empty argument when its value failed to parse, which cleared an earlier rule's value; browsers ignore an invalid declaration, so it is left out. The CSS-wide keyword cases join the existing keyword suite.
… none A filter or transform list that a var() fills with `none`, a cleared value or `initial` is `none` alone or invalid at computed-value time, and either way the property is `none`; the resolvers returned the keyword inside the list.
…rive cleared Only a shorthand that returns its longhands as an object receives a cleared argument: animation wraps its argument in a list, and box-shadow and drop-shadow handle it before the handler.
…he declaration A keyword is a value only on its own, so one inside a function, a colour or a list makes the whole declaration invalid; rejecting the token alone left `box-shadow: 1px 1px inherit` as a shadow and `blur(inherit)` as an empty blur. A var() or env() defers its value, and a custom property's value is any tokens, so neither is checked.
…CSS-wide keyword whole These three parse their own value, so a keyword inside it was dropped as a token and the rest applied: `object-position: left inherit` set `left`. They now share the declaration's keyword decision, so the declaration is invalid and an earlier rule's value stands, as it does in Chromium 153, Firefox 155 and WebKit 26.6.
… the function is A fallback replaces its var() or env(), so a CSS-wide keyword in it applies only when the function is the declaration's whole value (css-variables-1 §3). Inside a longer value the keyword leaves the declaration invalid at computed-value time, yet it was resolved everywhere: `color-mix(in srgb, var(--missing, inherit), blue)` mixed the inherited colour with blue, and `translateX(var(--missing, initial))` compiled `unset` into the function. Inside a longer value the fallback is now dropped with a warning. An env() fallback is parsed for the declaring property, as a var() fallback is, rather than under the env() name.
A keyword on its own was reported lowercased, while a nested one and every other value warning quote the value as the stylesheet spells it.
YevheniiKotyrlo
force-pushed
the
fix/color-inherit
branch
from
October 11, 2026 12:35
e3049ba to
5eddc33
Compare
This was referenced Oct 11, 2026
color: inherit to the inherited-color variable
This was referenced Oct 11, 2026
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 17:50
A custom property set to inherit, unset, revert or revert-layer was dropped, so an earlier rule's value on the same element won, and one set to initial was dropped with a warning and read the ancestor's value. Each now compiles to a value the runtime resolves where the property is declared: inherit reads the parent's value over every declaration on the element, and initial is the guaranteed-invalid value, which takes a var() fallback and hides an ancestor's declaration (css-variables-1 §2). A declaration a var() leaves without a value is invalid at computed-value time and behaves as unset (css-variables-1 §3.1): color inherits, a runtime-parsed shorthand resets its longhands, and any other property is cleared. A filter list with such an entry has no filter, an empty fallback substitutes nothing, and the inliner never folds an inheriting keyword and compiles a read of an inlined initial the same way. A context lookup reads only a variable's own entries, so var(--constructor) takes its fallback.
…it sits A var() with no fallback for a variable that has no value left its declaration a value when it sat inside a list, a function or another custom property: `--reader: var(--source) 1px` read `1px`, and a custom property whose value failed fell back to its ancestor's declaration instead of the reader's fallback. The lookup now records the failed substitution itself, so the declaration is invalid at computed-value time and a custom property holding one is the guaranteed-invalid value (css-variables-1 §3.1). A channel rncss publishes for a computed value, such as the em, is a default when nothing publishes it, never a failure. The inliner flattens every variable a value reads, inside functions and fallbacks too, before substituting it, so the result no longer depends on declaration order, and leaves each member of a cycle to the runtime. A custom property reading the guaranteed-invalid value is guaranteed-invalid itself, a declaration whose value substitutes to nothing is unset, and an empty custom property keeps its empty value, which substitutes nothing inside a list at runtime as well.
…me too The compiler made a declaration whose value substitutes to nothing unset, while the runtime handed React Native the empty list it left: `margin: var(--a,) var(--b,)` with neither declared rendered `margin: []` over an earlier rule's value. The list resolver records when every entry substituted nothing, which leaves a property's value invalid at computed-value time and a custom property's value empty, as css-variables-1 §3.1 defines.
… left to the runtime
…ation that reads it
…h the compiler resolves
YevheniiKotyrlo
marked this pull request as ready for review
October 17, 2026 00:11
YevheniiKotyrlo
marked this pull request as draft
October 17, 2026 01:09
The animation shorthand answered `unset` with nothing, so `animation: initial`, `animation: unset` and `animation: var(--missing)` left an earlier rule's animation running. It now resets every longhand it sets, as the other shorthands do.
YevheniiKotyrlo
marked this pull request as ready for review
October 17, 2026 02:30
YevheniiKotyrlo
marked this pull request as draft
October 19, 2026 22:07
YevheniiKotyrlo
marked this pull request as ready for review
October 20, 2026 11:59
YevheniiKotyrlo
marked this pull request as draft
October 28, 2026 19:06
…ts element inherited An element publishes its `color` to its subtree as the declaration it wrote, over the colour it inherited. When that declaration reads `currentcolor` through a variable, as `color: var(--c)` with `--c: currentcolor` does, a descendant that resolves the publication reaches the inherited-colour lookup, which reads the same publication again, so the colour cycles and the descendant gets none. CSS hands the descendant the element's computed colour, which is what it inherited. The variables an element hands its subtree now keep the ones it inherited, and a lookup reached while resolving a published colour reads the colour from there, one scope up. A reader guards that link, so it renders again when an ancestor's colour changes. The compiler no longer publishes the bare lookup for a colour that reads the inherited one, so a child of `color: color-mix(in srgb, currentcolor, blue)` inherits the mix, as CSS computes it.
The variables an element handed its subtree linked the ones it inherited whenever it declared a variable, and the copies `VariableContextProvider` and `vars()` make dropped the link. Below an element that declares a variable but publishes no colour, a child of `color: color-mix(in srgb, currentcolor, blue)` resolved the mix against the mix itself and mixed twice; below a provider it got no colour. The link is now set where an element publishes its colour and carried through every other copy, so `currentcolor` inside a published colour reads what its publisher inherited wherever the reader sits. The rule `light-dark()` adds for its dark value kept the variables of the rule it came from, so in dark mode it published the light colour and a child inherited that. It now publishes the dark colour it applies. A published colour invalid at computed-value time is `unset`, so its subtree inherits what the element inherited, as CSS computes it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
On native,
color: inherit, and so Tailwind'stext-inherit, is dropped with a warning, so a label under a coloured button falls back to React Native's default colour where web renders the parent's. The CSS-wide keywords around it diverge from CSS in the same path:unsetoncoloris dropped, andcurrentcolorinsidecolorreads the element's own colour, socolor: currentcolorresolves into itself.revertandrevert-layerreach the style as strings, andinitialis dropped, so an earlier rule's value survives.box-shadow: 1px 1px inheritclears an earlier shadow andobject-position: left inheritappliesleft.unsetpublishes the literal,inheritandinitialare dropped, a keyword fallback is dropped, and anenv()fallback is parsed under the variable's name.INHERITmisses.animationkeeps running.color: inheritstill publishes its colour to the subtree.var()resolves to nothing is left out where browsers make itunset, and inside a list the entry is dropped.var()holding several transforms.Solution
var()orenv()that is the whole value (css-values-4 §2.1); inside a longer value the declaration is ignored.revertandrevert-layerroll back tounset, since React Native has no user-agent or user origin (css-cascade-5 §7.3.4);unsetisinheritoncolorand on custom properties andinitialelsewhere (§7.3.3).inheritoncolorreads the inherited colour; on a custom property it is the parent's value, read over every declaration on the element (css-variables-1 §2); elsewhere it is dropped with a warning.initialclears the property, which leaves React Native's own default in place, and on a custom property it is the guaranteed-invalid value.var()with no value and no fallback leaves its declaration invalid at computed-value time wherever it sits (css-variables-1 §3.1), so the declaration behaves asunset. An empty fallback substitutes nothing.unset.currentcolorinsidecolorresolves against the parent (css-color-4 §6.4), and elsewhere reads the element's own colour.light-dark()'s dark rule included, and links the variables it hands down to the ones it inherited. Every other copy of those variables keeps the link, another element's,VariableContextProvider's andvars()'s, so acurrentcolorthe published colour reads, written or through a variable, resolves against the publisher's parent wherever the reader sits: a child ofcolor: var(--c)with--c: currentcolorinherits the parent's colour, and a child ofcolor: color-mix(in srgb, currentcolor, blue)inherits the mix. A published colour invalid at computed-value time isunset, so its subtree inherits what the element inherited. A reader renders again when that colour changes.light-dark()is parsed once, each declaration keeps its own target, andvar(--constructor)takes its fallback.Tests
compiler.test.tsx,colors.test.tsx,keywords.test.tsx,variables.test.tsx,transform.test.tsx,filter-list-runtime.test.tsx,animations.test.tsxand Tailwind'stypography.test.tsxcover each keyword oncolor, custom properties, plain properties and shorthands, as a whole value, nested and as a fallback, on the inlined and the kept route, a colour read through a kept variable, a subtree following an ancestor's change, a published colour reaching a child through an element or a provider that publishes none, andlight-dark()in dark mode. Onmain, 259 of the 498 cases fail; all pass here, and undoing each part of the change fails its own cases. Every expectation is what Chromium, Firefox and WebKit compute, and the three agree (153, 155 and 26.6; 156, 157 and 27.2 for the published colour).Verification
Every job of the workflow passes on this branch merged into
main: build, lint, typecheck,yarn test,node --test, both library builds, the web export, commitlint, and the iOS dev build with Xcode 27.1, where CI pins 26.6.Device evidence, before and after: #391 (comment)
Related
#464 keeps
color-mix()intact through the JSON a device loads its stylesheet as, which thecolor-mix()ofcurrentcolorcases need there. #503, stacked on this branch, keeps a reader's fallback where an inlined value still reads a variable only the runtime resolves. #389 resolves atranslateheld in a variable, and #411 scopes a pseudo-element's custom properties to it, which supersedes the pseudo-element change here.#501 builds on this and #474, so both land first. #392 seeds the root colour that the
colors.test.tsxandtypography.test.tsxcases expectingmain'sPlatformColorseed read, so whichever of the two lands second updates them. This overlaps #413, #420, #421, #460 and #469, and sharesnative/styles/functions/filters.tswith #489, which readstext-shadowwithoutshorthandHandler; I'll re-cut whichever lands after the others, carrying this reset of a cleared shorthand intotext-shadow.tsif #489 lands first.